/*
Infomation
==========================================================================================
jQuery Plugin
	Name       : jquery.ajaxComboBox
	Version    : 4.2
	Update     : 2012-01-11
	Author     : sutara_lumpur
	Author-URI : http://d.hatena.ne.jp/sutara_lumpur/20090124/1232781879
	License    : MIT License (http://www.opensource.org/licenses/mit-license.php)
	Based-on   : Uses code and techniques from following libraries...
		* jquery.suggest 1.1
			Author     : Peter Vulgaris
			Author-URI : http://www.vulgarisoip.com/
==========================================================================================

Contents
==========================================================================================
01.変数・部品の定義
	変数の初期化
	部品の定義
	部品をページに配置

02.イベントハンドラ
	全件取得ボタン
	テキスト入力エリア
	ページナビ
	サブ情報
	body全体

03.初期値
	ComboBoxに初期値を挿入
	データベースではなく、JSONから初期値を得る
	初期化用Ajax後の処理

04.ボタン
	ボタンのtitle属性 初期
	ボタンのtitle属性 リスト展開中
	ボタンのtitle属性 ロード中
	ボタンの画像の位置を調整する
	ロード画像の表示・解除

05.未分類
	選択候補を追いかけて画面をスクロール
	タイマーによる入力値変化監視
	キー入力への対応

06.Ajax
	Ajaxの中断
	データベースへの問い合わせ
	データベースではなく、JSONを検索
	searchInsteadOfDB内のsort用の比較関数
	問い合わせ後の処理

07.ページナビ
	ナビ部分を作成
	1ページ目へ
	前のページへ
	次のページへ
	最後のページへ

08.候補リスト
	候補一覧の<ul>成形、表示
	現在選択中の候補の情報を取得
	現在選択中の候補に決定する
	選択候補を次に移す
	選択候補を前に移す
	候補の消去を本当に実行するか判断
	候補エリアを消去

09.サブ情報
	サブ情報で頻繁に使用する要素のサイズを算出
	サブ情報を表示する

10.処理の始まり
	オプション
	メッセージを言語別に用意

==========================================================================================
*/
(function($) {
	$.ajaxComboBox = function(combo_input, source, options, msg) {

		//Ajaxにおけるキャッシュを無効にする
		$.ajaxSetup({cache: false});
		
		//================================================================================
		//01.変数・部品の定義
		//--------------------------------------------------------------------------------
		//**********************************************
		//変数の初期化
		//**********************************************
		var show_hide        = false; //候補を、タイマー処理で表示するかどうかの予約
		var timer_show_hide  = false; //タイマー。フォーカスが外れた後、候補を非表示にするか
		var timer_delay      = false; //hold timeout ID for suggestion result_area to appear
		var timer_val_change = false; //タイマー変数(一定時間ごとに入力値の変化を監視)
		var type_suggest     = false; //リストのタイプ。false=>全件 / true=>予測
		var page_num_all     = 1;     //全件表示の際の、現在のページ番号
		var page_num_suggest = 1;     //候補表示の際の、現在のページ番号
		var max_all          = 1;     //全件表示の際の、全ページ数
		var max_suggest      = 1;     //候補表示の際の、全ページ数
		var now_loading      = false; //Ajaxで問い合わせ中かどうか？
		var reserve_btn      = false; //ボタンの背景色変更の予約があるかどうか？
		var reserve_click    = false; //マウスのキーを押し続ける操作に対応するためmousedownを検知
		var $xhr             = false; //XMLHttpオブジェクトを格納
		var key_paging       = false; //キーでページ移動したか？
		var key_select       = false; //キーで候補移動したか？？
		var prev_value       = '';    //ComboBoxの、以前の値

		//サブ情報
		var size_navi        = null;  //サブ情報表示用(ページナビの高さ)
		var size_results     = null;  //サブ情報表示用(リストの上枠線)
		var size_li          = null;  //サブ情報表示用(候補一行分の高さ)
		var size_left        = null;  //サブ情報表示用(リストの横幅)
		var select_field;             //サブ情報表示の場合に取得するカラム
		if(options.sub_info){
			if(options.show_field && !options.hide_field){
				select_field = options.field + ',' + options.show_field;
			} else {
				select_field = '*';
			}
		} else {
			select_field = options.field;
			options.hide_field = '';
		}
		if(options.select_only && select_field != '*'){
			select_field += ',' + options.primary_key;
		}

		//セレクト専用時、フォーム送信する一意の情報を格納する
		var primary_key = (options.select_only)
			? options.primary_key
			: '';

		//**********************************************
		//部品の定義
		//**********************************************
		//ComboBox本体
		$(combo_input)
			.attr('autocomplete', 'off')
			.addClass(options.input_class)
			.wrap('<div>');

		var $container = $(combo_input).parent().addClass(options.container_class);
		
		var $button = $('<div>').addClass(options.button_class);
		var $img = $('<img>').attr('src', options.button_img);
		$container.append($button);
		
		$button.append($img); 
		$container.append('<div style="clear:left"></div>');

		//サジェストリスト
		var $result_area = $('<div></div>')
			.addClass(options.re_area_class);

		var $navi = $('<div></div>')
			.addClass(options.navi_class);

		var $results = $('<ul></ul>')
			.addClass(options.results_class);

		$result_area.append($navi).append($results);
		
		$container.after($result_area);
		//サブ情報
		var $attached_dl_set = $('<div></div>')
			.addClass(options.sub_info_class);

		//"セレクト専用"オプション用
		var $hidden = $('<input type="hidden" />')
			.attr({
				'name': $(combo_input).attr('name'),
				'id'  : $(combo_input).attr('name') + '_hidden'
			})
			.val(''); 
		//**********************************************
		//部品をページに配置
		//**********************************************
		 //alert(combo_input);
		//セレクト専用時、hiddenを追加 
		//$("testone").append("<input type=button value=ceshi>");
		 if(options.select_only) $(combo_input).append($hidden); 
          
		//ComboBoxの幅を決定
		$container.width(
			$(combo_input).width() +
			parseInt($(combo_input).css('margin-left'), 10) +
			parseInt($(combo_input).css('margin-right'), 10) +
			parseInt($(combo_input).css('padding-left'), 10) +
			parseInt($(combo_input).css('padding-right'), 10) +
			parseInt($(combo_input).css('border-left-width'), 10) +
			parseInt($(combo_input).css('border-right-width'), 10) +
			$button.width() +
			parseInt($button.css('margin-left'), 10) +
			parseInt($button.css('margin-right'), 10) +
			parseInt($button.css('padding-left'), 10) +
			parseInt($button.css('padding-right'), 10) +
			parseInt($button.css('border-left-width'), 10) +
			parseInt($button.css('border-right-width'), 10)
		);
		
		//ボタンの高さ、タイトル属性、画像の位置
		$button.height(
			$(combo_input).height() +
			parseInt($(combo_input).css('padding-top'), 10) +
			parseInt($(combo_input).css('padding-bottom'), 10)
		);
		
		btnAttrDefault();
		btnPositionAdjust();

		//ComboBoxに初期値を挿入
		setInitVal();

		//================================================================================
		//02.イベントハンドラ
		//--------------------------------------------------------------------------------
		//**********************************************
		//全件取得ボタン
		//**********************************************
		$button.mouseup(function(ev) {
			if($result_area.css('display') == 'none') {
				clearInterval(timer_val_change);
				
				type_suggest = false;
				suggest();
				$(combo_input).focus();
			} else {
				hideResult();
			}
			ev.stopPropagation();
		});
		$button.mouseover(function() {
			reserve_btn = true;
			if (now_loading) return;
			$button
				.addClass(options.btn_on_class)
				.removeClass(options.btn_out_class);
		});
		$button.mouseout(function() {
			reserve_btn = false;
			if (now_loading) return;
			$button
				.addClass(options.btn_out_class)
				.removeClass(options.btn_on_class);
		});
		//最初はmouseoutの状態
		$button.mouseout();

		//**********************************************
		//テキスト入力エリア
		//**********************************************
		//前処理(クロスブラウザ用)
		if(window.opera){
			//Opera用
			$(combo_input).keypress(processKey);
		}else{
			//その他用
			$(combo_input).keydown(processKey);
		}
		
		$(combo_input).focus(function() {
			show_hide = true;
			checkValChange();
		});
		$(combo_input).blur(function(ev) {
			
			//入力値の監視を中止
			clearTimeout(timer_val_change);

			//候補消去を予約
			show_hide = false;

			//消去予約タイマーをセット
			checkShowHide();

			//セレクト状態を確認
			btnAttrDefault();
		});
		$(combo_input).mousedown(function(ev) {
			reserve_click = true;

			//消去予約タイマーを中止
			clearTimeout(timer_show_hide);

			ev.stopPropagation();
		});
		$(combo_input).mouseup(function(ev) {
			$(combo_input).focus();
			reserve_click = false;
			ev.stopPropagation();
		});

		//**********************************************
		//ページナビ
		//**********************************************
		$navi.mousedown(function(ev) {
			reserve_click = true;

			//消去予約タイマーを中止
			clearTimeout(timer_show_hide);

			ev.stopPropagation();
		});
		$navi.mouseup(function(ev) {
			$(combo_input).focus();
			reserve_click = false;
			ev.stopPropagation();
		});

		//**********************************************
		//サブ情報
		//**********************************************
		$attached_dl_set.mousedown(function(ev) {
			reserve_click = true;

			//消去予約タイマーを中止
			clearTimeout(timer_show_hide);
			ev.stopPropagation();
		});
		$attached_dl_set.mouseup(function(ev) {
			$(combo_input).focus();
			reserve_click = false;
			ev.stopPropagation();
		});

		//**********************************************
		//body全体
		//**********************************************
		$('body').mouseup(function() {
			//消去予約タイマーを中止
			clearTimeout(timer_show_hide);

			//候補を消去する
			show_hide = false;
			hideResult();
		});

		//================================================================================
		//03.初期値
		//--------------------------------------------------------------------------------
		//**********************************************
		//ComboBoxに初期値を挿入
		//**********************************************
		function setInitVal(){
			if(options.init_val === false) return;

			if(options.select_only){
				//------------------------------------------
				//セレクト専用への値挿入
				//------------------------------------------
				//hiddenへ値を挿入
				var q_word = options.init_val;
				$hidden.val(q_word);

				//テキストボックスへ値を挿入
				var init_val_data = '';
				if(typeof options.source == 'object'){
					//sourceがデータセットの場合
					initInsteadOfDB(q_word);
				}else{
					var $xhr2 = $.get(
						options.init_src,
						{
							'q_word'      : q_word,
							'field'       : options.field,
							'primary_key' : options.primary_key,
							'db_table'    : options.db_table
						},
						function(data){ afterInit(data) }
					);
				}
			} else {
				//------------------------------------------
				//通常の、テキストボックスへの値挿入
				//------------------------------------------
				prev_value = options.init_val;
				$(combo_input).val(options.init_val);
			}
		}
		//**********************************************
		//データベースではなく、JSONから初期値を得る
		//**********************************************
		function initInsteadOfDB(q_word){
			for(var i=0; i<options.source.length; i++){
				if(options.source[i][options.primary_key] == q_word){
					var data = options.source[i][options.field];
					break;
				}
			}
			afterInit(data);
		}
		//**********************************************
		//初期化用Ajax後の処理
		//**********************************************
		function afterInit(data){
			$(combo_input).val(data);
			prev_value = data;

			//選択状態
			$(combo_input)
				.attr('title',msg['select_ok'])
				.removeClass(options.select_ng_class)
				.addClass(options.select_ok_class);
		}
		
		//================================================================================
		//04.ボタン
		//--------------------------------------------------------------------------------
		//**********************************************
		//ボタンのtitle属性 初期
		//**********************************************
		function btnAttrDefault() {

			if(options.select_only){

				if($(combo_input).val() != ''){
					if($hidden.val() != ''){

						//選択状態
						$(combo_input)
							.attr('title',msg['select_ok'])
							.removeClass(options.select_ng_class)
							.addClass(options.select_ok_class);
					} else {

						//入力途中
						$(combo_input)
							.attr('title',msg['select_ng'])
							.removeClass(options.select_ok_class)
							.addClass(options.select_ng_class);
					}
				} else {
					//完全な初期状態へ戻す
					$hidden.val('');
				}
			}
			//初期状態
			$button.attr('title', msg['get_all_btn']);
			$img.attr('src', options.button_img);
			btnPositionAdjust();
		}
		//**********************************************
		//ボタンのtitle属性 リスト展開中
		//**********************************************
		function btnAttrClose() {
			$button.attr('title',msg['close_btn']);
			$img.attr('src', options.load_img);
			btnPositionAdjust();
		}
		//**********************************************
		//ボタンのtitle属性 ロード中
		//**********************************************
		function btnAttrLoad() {
			$button.attr('title',msg['loading']);
			$img.attr('src', options.load_img);
			btnPositionAdjust();
		}
		//**********************************************
		//ボタンの画像の位置を調整する
		//**********************************************
		function btnPositionAdjust(){
			var width_btn = $button.width() + 
			parseInt($button.css('padding-left'), 10) +
			parseInt($button.css('padding-right'), 10);
			
			var height_btn = $button.height() + 
			parseInt($button.css('padding-top'), 10) +
			parseInt($button.css('padding-bottom'), 10);
			
			var width_img = $img.width();
			var height_img = $img.height();
			
			var left = width_btn / 2 - (width_img / 2);
			var top = height_btn / 2 - (height_img / 2);
			
			$img.css({
				'top':top,
				'left':left
			});
		}
		//**********************************************
		//ロード画像の表示・解除
		//**********************************************
		function setLoadImg() {
			now_loading = true;
			btnAttrLoad();
		}
		function clearLoadImg() {
			$img.attr('src' , options.button_img);
			now_loading = false;
			if(reserve_btn) $button.mouseover(); else $button.mouseout();
		}

		//================================================================================
		//05.未分類
		//--------------------------------------------------------------------------------
		//**********************************************
		//選択候補を追いかけて画面をスクロール
		//**********************************************
		//キー操作による候補移動、ページ移動のみに適用
		//
		// @param boolean enforce 移動先をテキストボックスに強制するか？
		function scrollWindow(enforce) {

			//------------------------------------------
			//使用する変数を定義
			//------------------------------------------
			var $current_result = getCurrentResult();

			var target_top = ($current_result && !enforce)
				? $current_result.offset().top
				: $container.offset().top;

			var target_size;
			if(options.sub_info){
				target_size =
					$attached_dl_set.height() +
					parseInt($attached_dl_set.css('border-top-width'), 10) +
					parseInt($attached_dl_set.css('border-bottom-width'), 10);

			} else {
				setSizeLi();
				target_size = size_li;
			}

			var client_height = document.documentElement.clientHeight;

			var scroll_top = (document.documentElement.scrollTop > 0)
				? document.documentElement.scrollTop
				: document.body.scrollTop;

			var scroll_bottom = scroll_top + client_height - target_size;

			//------------------------------------------
			//スクロール処理
			//------------------------------------------
			var gap;
			if ($current_result.length) {
				if(target_top < scroll_top || target_size > client_height) {
					//上へスクロール
					//※ブラウザの高さがターゲットよりも低い場合もこちらへ分岐する。
					gap = target_top - scroll_top;

				} else if (target_top > scroll_bottom) {
					//下へスクロール
					gap = target_top - scroll_bottom;

				} else {
					//スクロールは行われない
					return;
				}

			} else if (target_top < scroll_top) {
				gap = target_top - scroll_top;
			}
			window.scrollBy(0, gap);
		}

		//**********************************************
		//タイマーによる入力値変化監視
		//**********************************************
		function checkValChange() {
			timer_val_change = setTimeout(isChange,500);

			function isChange() {
				now_value = $(combo_input).val();

				if(now_value != prev_value) {
					//sub_info属性を削除
					$(combo_input).removeAttr('sub_info');

					//セレクト専用時
					if(options.select_only){
						$hidden.val('');
						btnAttrDefault();
					}
					//ページ数をリセット
					page_num_suggest = 1;
					
					type_suggest = true;
					suggest(true);
				}
				prev_value = now_value;

				//一定時間ごとの監視を再開
				checkValChange();
			}
		}

		//**********************************************
		//キー入力への対応
		//**********************************************
		function processKey(e) {
			if (
				(/27$|38$|40$|^9$/.test(e.keyCode) && $result_area.is(':visible')) ||
				(/^37$|39$|13$|^9$/.test(e.keyCode) && getCurrentResult()) ||
				/40$/.test(e.keyCode)
			) {
				if (e.preventDefault)  e.preventDefault();
				if (e.stopPropagation) e.stopPropagation();

				e.cancelBubble = true;
				e.returnValue  = false;

				switch(e.keyCode) {
					case 37: // left
						if (e.shiftKey) firstPage();
						else            prevPage();
						break;

					case 38: // up
						key_select = true;
						prevResult();
						break;

					case 39: // right
						if (e.shiftKey) lastPage();
						else            nextPage();
						break;

					case 40: // down
						if (!$result_area.is(':visible') && !getCurrentResult()){
							type_suggest = false;
							suggest();
						} else {
							key_select = true;
							nextResult();
						}
						break;

					case 9:  // tab
						key_paging = true;
						hideResult();
						break;

					case 13: // return
						selectCurrentResult(true);
						break;

					case 27: //	escape
						key_paging = true;
						hideResult();
						break;
				}

			} else {
				checkValChange();
			}
		}

		//================================================================================
		//06.Ajax
		//--------------------------------------------------------------------------------
		//**********************************************
		//Ajaxの中断
		//**********************************************
		function abortAjax() {
			if ($xhr){
				$xhr.abort();
				$xhr = false;
				clearLoadImg();
			}
		}

		//**********************************************
		//データベースへの問い合わせ
		//**********************************************
		function suggest(){
			//候補リスト取得後、第１候補を選択状態にするか? テキスト入力以外なら、それを行う。
			var select_first   = (arguments[0] == undefined) ? false : true;
			
			var q_word         = (type_suggest) ? $.trim($(combo_input).val()) : '';
			var which_page_num = (type_suggest) ? page_num_suggest : page_num_all;

			if (type_suggest && q_word.length < options.minchars){ 
				hideResult();
				
			} else {
				//Ajax通信をキャンセル
				abortAjax();

				//サブ情報消去
			$attached_dl_set.children('dl').hide();

				setLoadImg();

				if(typeof options.source == 'object'){
					//sourceがデータセットの場合
					searchInsteadOfDB(q_word, which_page_num, select_first);
				}else{
					//ここでAjax通信を行っている
					$xhr = $.getJSON(
						options.source,
						{
							'q_word'       : q_word,
							'page_num'     : which_page_num,
							'per_page'     : options.per_page,
							'field'        : options.field,
							'search_field' : options.search_field,
							'and_or'       : options.and_or,
							'show_field'   : options.show_field,
							'hide_field'   : options.hide_field,
							'select_field' : select_field,
							'order_field'  : options.order_field,
							'order_by'     : options.order_by,
							'primary_key'  : primary_key,
							'db_table'     : options.db_table
						},
						function(json_data){ afterAjax(json_data, q_word, which_page_num, select_first) }
					);
				}
			}
		}
		//**********************************************
		//データベースではなく、JSONを検索
		//**********************************************
		function searchInsteadOfDB(q_word, which_page_num, select_first){
			//正規表現のメタ文字をエスケープ
			var escaped_q = q_word.replace(/\W/g,'\\$&');
			escaped_q = escaped_q.toString();
		
			//SELECT * FROM source WHERE field LIKE q_word;
			var matched = [];
			var reg = new RegExp(escaped_q, 'gi');
			for(var i = 0; i < options.source.length; i++){
				if(options.source[i][options.field].match(reg)){
					matched[matched.length] = options.source[i];
				}
			}
			var json_data = {};
			json_data['cnt'] = matched.length;
			if(!json_data['cnt']){
				json_data['candidate'] = false;

			}else{

				//ORDER BY (CASE WHEN ...), order_field ASC (or DESC)
				var matched1 = [];
				var matched2 = [];
				var matched3 = [];
				var reg1 = new RegExp('^' + escaped_q + '$', 'gi');
				var reg2 = new RegExp('^' + escaped_q, 'gi');
				for(var i = 0; i < matched.length; i++){
					if(matched[i][options.order_field].match(reg1)){
						matched1.push(matched[i]);
					}else if(matched[i][options.order_field].match(reg2)){
						matched2.push(matched[i]);
					}else{
						matched3.push(matched[i]);
					}
				}				
				if(options.order_by == 'ASC'){
					matched1.sort(compareASC);
					matched2.sort(compareASC);
					matched3.sort(compareASC);
				}else{
					matched1.sort(compareDESC);
					matched2.sort(compareDESC);
					matched3.sort(compareDESC);
				}
				var sorted = matched1.concat(matched2);
				sorted = sorted.concat(matched3);
				
				//LIMIT xx OFFSET xx
				var start = (which_page_num - 1) * options.per_page;
				var end   = start + options.per_page;
				
				//----------------------------------------------
				//最終的に返るオブジェクトを作成
				//----------------------------------------------
				var show_field = options.show_field.split(',');
				var hide_field = options.hide_field.split(',');
				for(var i = start, sub = 0; i < end; i++, sub++){
					if(sorted[i] == undefined) break;
				
					for(var key in sorted[i]){
						//セレクト専用
						if(key == primary_key){
							if(json_data['primary_key'] == undefined){
								json_data['primary_key'] = [];
							}
							json_data['primary_key'].push(sorted[i][key]);
						}
					
						if(key == options.field){
							//変換候補を取得
							if(json_data['candidate'] == undefined){
								json_data['candidate'] = [];
							}
							json_data['candidate'].push(sorted[i][key]);
						} else {
							//サブ情報
							if($.inArray(key, hide_field) == -1){
								if(
									show_field !== false
									&& !$.inArray('*', show_field) > -1
									&& !$.inArray(key, show_field)
								){
									continue;
								}
								if(json_data['attached'] == undefined){
									json_data['attached'] = [];
								}
								if(json_data['attached'][sub] == undefined){
									json_data['attached'][sub] = [];
								}
								json_data['attached'][sub].push([key, sorted[i][key]]);
							}
						}
					}
				}
				json_data['cnt_page'] = json_data['candidate'].length;
			}
			afterAjax(json_data, q_word, which_page_num, select_first);
		}
		//**********************************************
		//searchInsteadOfDB内のsort用の比較関数
		//**********************************************
		function compareASC(a, b){
			return a[options.order_field].localeCompare(b[options.order_field]);
		}
		function compareDESC(a, b){
			return b[options.order_field].localeCompare(a[options.order_field]);
		}
		//**********************************************
		//問い合わせ後の処理
		//**********************************************
		function afterAjax(json_data, q_word, which_page_num, select_first){
		
			if(!json_data.candidate){
				//一致するデータ見つからなかった
				hideResult();
			} else {
				//全件数が1ページ最大数を超えない場合、ページナビは非表示
				if(json_data.cnt > json_data.cnt_page){
					setNavi(json_data.cnt, json_data.cnt_page, which_page_num);
				} else {
					$navi.css('display','none');
				}

				//候補リスト(arr_candidate)
				var arr_candidate = json_data.candidate;
				/*
				ヒット文字への下線の付加を、中止しました。 2012/01/11
				
				var arr_candidate = [];
				//正規表現のメタ文字をエスケープ
				var escaped_q = q_word.replace(/\W/g,'\\$&');
				$.each(json_data.candidate, function(i,obj){
					arr_candidate[i] = obj.replace(
						new RegExp(escaped_q, 'ig'),
						function(hit) {
							return '<span class="' + options.match_class + '">' + hit + '</span>';
						}
					);
				});
				*/
				
				//サブ情報(arr_attached)
				var arr_attached = [];
				if(json_data.attached  && options.sub_info){
					$.each(json_data.attached,function(i,obj){
						arr_attached[i] = obj;
					});
				} else {
					arr_attached = false;
				}

				//セレクト専用(arr_primary_key)
				var arr_primary_key = [];
				if(json_data.primary_key){
					$.each(json_data.primary_key,function(i,obj){
						arr_primary_key[i] = obj;
					});
				} else {
					arr_primary_key = false;
				}
				displayItems(arr_candidate, arr_attached, arr_primary_key);
			}
			clearLoadImg();
			if(!select_first) selectFirstResult(); //テキスト入力以外なら、第１候補を選択状態にする
		}
		//================================================================================
		//07.ページナビ
		//--------------------------------------------------------------------------------
		//**********************************************
		//ナビ部分を作成
		//**********************************************
		// @param integer cnt         DBから取得した候補の数
		// @param integer page_num    全件、または予測候補の一覧のページ数
		function setNavi(cnt, cnt_page, page_num) {

			var num_page_top = options.per_page * (page_num - 1) + 1;
			var num_page_end = num_page_top + cnt_page - 1;

			var cnt_result = msg['page_info']
				.replace('cnt'          , cnt)
				.replace('num_page_top' , num_page_top)
				.replace('num_page_end' , num_page_end);

			$navi.text(cnt_result);

			var navi_p = $('<p></p>'); //ページング部分のオブジェクト
			var max    = Math.ceil(cnt / options.per_page); //全ページ数

			//ページ数
			if (type_suggest) {
				max_suggest = max;
			}else{
				max_all = max;
			}

			//表示する一連のページ番号の左右端
			var left  = page_num - Math.ceil ((options.navi_num - 1) / 2);
			var right = page_num + Math.floor((options.navi_num - 1) / 2);

			//現ページが端近くの場合のleft,rightの調整
			while(left < 1){ left ++;right++; }
			while(right > max){ right--; }
			while((right-left < options.navi_num - 1) && left > 1){ left--; }

			//----------------------------------------------
			//ページング部分を作成

			//『<< 前へ』の表示
			if(page_num == 1) {
				if(!options.navi_simple){
					$('<span></span>')
						.text('<< 1')
						.addClass('page_end')
						.appendTo(navi_p);
				}
				$('<span></span>')
					.text(msg['prev'])
					.addClass('page_end')
					.appendTo(navi_p);
			} else {
				if(!options.navi_simple){
					$('<a></a>')
						.attr({'href':'javascript:void(0)','class':'navi_first'})
						.text('<< 1')
						.attr('title', msg['first_title'])
						.appendTo(navi_p);
				}
				$('<a></a>')
					.attr({'href':'javascript:void(0)','class':'navi_prev'})
					.text(msg['prev'])
					.attr('title', msg['prev_title'])
					.appendTo(navi_p);
			}

			//各ページへのリンクの表示
			for (i = left; i <= right; i++)
			{
				//現在のページ番号は<span>で囲む(強調表示用)
				var num_link = (i == page_num) ? '<span class="current">'+i+'</span>' : i;

				$('<a></a>')
					.attr({'href':'javascript:void(0)','class':'navi_page'})
					.html(num_link)
					.appendTo(navi_p);
			}

			//『次のX件 >>』の表示
			if(page_num == max) {
				$('<span></span>')
					.text(msg['next'])
					.addClass('page_end')
					.appendTo(navi_p);
				if(!options.navi_simple){
					$('<span></span>')
						.text(max + ' >>')
						.addClass('page_end')
						.appendTo(navi_p);
				}
			} else {
				$('<a></a>')
					.attr({'href':'javascript:void(0)','class':'navi_next'})
					.text(msg['next'])
					.attr('title', msg['next_title'])
					.appendTo(navi_p);
				if(!options.navi_simple){
					$('<a></a>')
						.attr({'href':'javascript:void(0)','class':'navi_last'})
						.text(max + ' >>')
						.attr('title', msg['last_title'])
						.appendTo(navi_p);
				}
			}

			//ページナビの表示、イベントハンドラの設定は必要な場合のみ行う
			if (max > 1) {
				$navi.append(navi_p).show();

				//----------------------------------------------
				//ページング部分のイベントハンドラ

				//『<< 1』をクリック
				$('.navi_first').mouseup(function(ev) {
					$(combo_input).focus();
					ev.preventDefault();
					firstPage();
				});

				//『< 前へ』をクリック
				$('.navi_prev').mouseup(function(ev) {
					$(combo_input).focus();
					ev.preventDefault();
					prevPage();
				});

				//各ページへのリンクをクリック
				$('.navi_page').mouseup(function(ev) {
					$(combo_input).focus();
					ev.preventDefault();

					if(!type_suggest){
						page_num_all = parseInt($(this).text(), 10);
					}else{
						page_num_suggest = parseInt($(this).text(), 10);
					}
					suggest();
				});

				//『次へ >』をクリック
				$('.navi_next').mouseup(function(ev) {
					$(combo_input).focus();
					ev.preventDefault();
					nextPage();
				});

				//『max >>』をクリック
				$('.navi_last').mouseup(function(ev) {
					$(combo_input).focus();
					ev.preventDefault();
					lastPage();
				});
			}
		}

		//**********************************************
		//1ページ目へ
		//**********************************************
		function firstPage() {
			if(!type_suggest) {
				if (page_num_all > 1) {
					page_num_all = 1;
					suggest();
				}
			}else{
				if (page_num_suggest > 1) {
					page_num_suggest = 1;
					suggest();
				}
			}
		}
		//**********************************************
		//前のページへ
		//**********************************************
		function prevPage() {
			if(!type_suggest){
				if (page_num_all > 1) {
					page_num_all--;
					suggest();
				}
			}else{
				if (page_num_suggest > 1) {
					page_num_suggest--;
					suggest();
				}
			}
		}
		//**********************************************
		//次のページへ
		//**********************************************
		function nextPage() {
			if(!type_suggest){
				if (page_num_all < max_all) {
					page_num_all++;
					suggest();
				}
			} else {
				if (page_num_suggest < max_suggest) {
					page_num_suggest++;
					suggest();
				}
			}
		}
		//**********************************************
		//最後のページへ
		//**********************************************
		function lastPage() {
			if(!type_suggest){
				if (page_num_all < max_all) {
					page_num_all = max_all;
					suggest();
				}
			}else{
				if (page_num_suggest < max_suggest) {
					page_num_suggest = max_suggest;
					suggest();
				}
			}
		}

		//================================================================================
		//08.候補リスト
		//--------------------------------------------------------------------------------
		//**********************************************
		//候補一覧の<ul>成形、表示
		//**********************************************
		// @params array arr_candidate   DBから検索・取得した値の配列
		// @params array arr_attached    サブ情報の配列
		// @params array arr_primary_key 主キーの配列
		//
		//arr_candidateそれぞれの値を<li>で囲んで表示。
		//同時に、イベントハンドラを記述。
		function displayItems(arr_candidate, arr_attached, arr_primary_key) {

			if (arr_candidate.length == 0) {
				hideResult();
				return;
			}

			//候補リストを、一旦リセット
			$results.empty();
			$attached_dl_set.empty();
			for (var i = 0; i < arr_candidate.length; i++) {

				//候補リスト
				var $li = $('<li>').text(arr_candidate[i]); //!!! against XSS !!!
				
				//セレクト専用
				if(options.select_only){
					$li.attr('id', arr_primary_key[i]);
				}
				$results.append($li);

				//サブ情報のdlを生成
				if(arr_attached){
					//sub_info属性にJSON文字列そのままを格納
					var json_subinfo = '{';
					var $dl = $('<dl>');
					//テーブルの各行を生成
					for (var j=0; j < arr_attached[i].length; j++) {
						//sub_info属性の値を整える
						var json_key = arr_attached[i][j][0].replace('\'', '\\\'');
						var json_val = arr_attached[i][j][1].replace('\'', '\\\'');
						json_subinfo += "'" + json_key + "':" + "'" + json_val + "'";
						if((j+1) < arr_attached[i].length) json_subinfo += ',';

						//thの別名を検索する
						if(options.sub_as[arr_attached[i][j][0]] != null){
							var dt = options.sub_as[arr_attached[i][j][0]];
						} else {
							var dt =  arr_attached[i][j][0];
						}
						dt = $('<dt>').text(dt); //!!! against XSS !!!
						$dl.append(dt);
						
						var dd = $('<dd>').text(arr_attached[i][j][1]);	//!!! against XSS !!!
						$dl.append(dd);
					}
					//sub_info属性を候補リストのliに追加
					json_subinfo += '}';
					$li.attr('sub_info', json_subinfo);
					$attached_dl_set.append($dl);
					$attached_dl_set.children('dl').hide();
				}
			}
			//画面に表示
			if(arr_attached) $attached_dl_set.insertAfter($results);

			//サジェスト結果表示
			var offset = $container.offset();
			$result_area
				.show()
				.width(
					$container.width() -
					parseInt($result_area.css('border-left-width'), 10) -
					parseInt($result_area.css('border-right-width'), 10)
				)
				.css({
					'top':$container.height() +
						offset.top +
						parseInt($container.css('padding-top'), 10) +
						parseInt($container.css('padding-bottom'), 10) +
						parseInt($container.css('border-top-width'), 10) +
						parseInt($container.css('border-bottom-width'), 10)
				});
			$container.addClass(options.container_open_class);

			$results
				.children('li')
				.mouseover(function() {

					//Firefoxでは、候補一覧の上にマウスカーソルが乗っていると
					//うまくスクロールしない。そのための対策。イベント中断。
					if (key_select) {
						key_select = false;
						return;
					}

					//サブ情報を表示
					setSubInfo(this);

					$results.children('li').removeClass(options.select_class);
					$(this).addClass(options.select_class);
				})
				.mousedown(function(e) {
					reserve_click = true;

					//消去予約タイマーを中止
					clearTimeout(timer_show_hide);
					//ev.stopPropagation();
				})
				.mouseup(function(e) {
					reserve_click = false;

					//Firefoxでは、候補一覧の上にマウスカーソルが乗っていると
					//うまくスクロールしない。そのための対策。イベント中断。
					if (key_select) {
						key_select = false;
						return;
					}
					e.preventDefault();
					e.stopPropagation();
					selectCurrentResult(false);
				});

			//ボタンのtitle属性変更(閉じる)
			btnAttrClose();
		}

		//**********************************************
		//現在選択中の候補の情報を取得
		//**********************************************
		// @return object current_result 現在選択中の候補のオブジェクト(<li>要素)
		function getCurrentResult() {

			if (!$result_area.is(':visible')) return false;

			var $current_result = $results.children('li.' + options.select_class);

			if (!$current_result.length) $current_result = false;

			return $current_result;
		}
		//**********************************************
		//現在選択中の候補に決定する
		//**********************************************
		function selectCurrentResult(is_enter_key) {

			//選択候補を追いかけてスクロール
			scrollWindow(true);

			var $current_result = getCurrentResult();

			if ($current_result) {
				$(combo_input).val($current_result.text());
				//サブ情報があるならsub_info属性を追加・書き換え
				if(options.sub_info) $(combo_input).attr('sub_info', $current_result.attr('sub_info'));
				hideResult();

				//added
				prev_value = $(combo_input).val();

				//セレクト専用
				if(options.select_only){
					$hidden.val($current_result.attr('id'));
					btnAttrDefault();
				}
			}
			if(options.bind_to){
			 	//候補選択を引き金に、イベントを発火する
				$(combo_input).trigger(options.bind_to, is_enter_key);
			}
			$(combo_input).focus();  //テキストボックスにフォーカスを移す
			$(combo_input).change(); //テキストボックスの値が変わったことを通知
		}
		//**********************************************
		//選択候補を次に移す
		//**********************************************
		function nextResult() {
			var $current_result = getCurrentResult();

			if ($current_result) {

				//サブ情報を表示
				setSubInfo($current_result.next());

				$current_result
					.removeClass(options.select_class)
					.next()
						.addClass(options.select_class);
			}else{
				//サブ情報を表示
				setSubInfo($results.children('li:first-child'), 0);

				$results.children('li:first-child').addClass(options.select_class);
			}
			//選択候補を追いかけてスクロール
			scrollWindow();
		}
		//**********************************************
		//選択候補を前に移す
		//**********************************************
		function prevResult() {
			var $current_result = getCurrentResult();

			if ($current_result) {

				//サブ情報を表示
				setSubInfo($current_result.prev());

				$current_result
					.removeClass(options.select_class)
					.prev()
						.addClass(options.select_class);
			}else{
				//サブ情報を表示
				setSubInfo(
					$results.children('li:last-child'),
					($results.children('li').length - 1)
				);

				$results.children('li:last-child').addClass(options.select_class);
			}
			//選択候補を追いかけてスクロール
			scrollWindow();
		}
		//**********************************************
		//候補の消去を本当に実行するか判断
		//**********************************************
		function checkShowHide() {
			timer_show_hide = setTimeout(function() {
				if (show_hide == false && reserve_click == false){
					hideResult();
				}
			},500);
		}
		//**********************************************
		//候補エリアを消去
		//**********************************************
		function hideResult() {

			if (key_paging) {
				//選択候補を追いかけてスクロール
				scrollWindow(true);
				key_paging = false;
			}

			$result_area.hide();
			$container.removeClass(options.container_open_class);

			//サブ情報消去
			$attached_dl_set.children('dl').hide();

			//Ajax通信をキャンセル
			abortAjax();

			//ボタンのtitle属性初期化
			btnAttrDefault();
		}
		//**********************************************
		//候補一覧の1番目の項目を、選択状態にする
		//**********************************************
		function selectFirstResult(){
			$results.children('li:first-child').addClass(options.select_class);

			//サブ情報を表示
			setSubInfo($results.children('li:first-child'));
		}
		//================================================================================
		//09.サブ情報
		//--------------------------------------------------------------------------------
		//**********************************************
		//サブ情報で頻繁に使用する要素のサイズを算出
		//**********************************************
		function setSizeResults(){
			if(size_navi == null){
				size_navi =
					$navi.height() +
					parseInt($navi.css('border-top-width'), 10) +
					parseInt($navi.css('border-bottom-width'), 10) +
					parseInt($navi.css('padding-top'), 10) +
					parseInt($navi.css('padding-bottom'), 10);
			}
		}
		function setSizeNavi(){
			if(size_results == null){
				size_results = parseInt($results.css('border-top-width'), 10);
			}
		}
		function setSizeLi(){
			if(size_li == null){
				$obj = $results.children('li:first');
				size_li =
					$obj.height() +
					parseInt($obj.css('border-top-width'), 10) +
					parseInt($obj.css('border-bottom-width'), 10) +
					parseInt($obj.css('padding-top'), 10) +
					parseInt($obj.css('padding-bottom'), 10);
			}
		}
		function setSizeLeft(){
			if(size_left == null){
				size_left =
					$results.width() +
					parseInt($results.css('padding-left'), 10) +
					parseInt($results.css('padding-right'), 10) +
					parseInt($results.css('border-left-width'), 10) +
					parseInt($results.css('border-right-width'), 10);
			}
		}

		//**********************************************
		//サブ情報を表示する
		//**********************************************
		// @paramas object  obj   サブ情報を右隣に表示させる<li>要素
		// @paramas integer n_idx 選択中の<li>の番号(0～)
		function setSubInfo(obj, n_idx){

			//サブ情報を表示しない設定なら、ここで終了
			if(!options.sub_info) return;

			//サブ情報の座標設定用の基本情報
			//初回の設定だけで、後は呼び出されない。
			setSizeNavi();
			setSizeResults();
			setSizeLi();
			setSizeLeft();

			//現在の<li>の番号は？
			if(n_idx == null){
				n_idx = $results.children('li').index(obj);
			}

			//一旦、サブ情報全消去
			$attached_dl_set.children('dl').hide();

			//リスト内の候補を選択する場合のみ、以下を実行
			if(n_idx > -1){

				var t_top = 0;
				if($navi.css('display') != 'none') t_top += size_navi;
				t_top += (size_results + size_li * n_idx);
				var t_left = size_left;

				t_top  += 'px';
				t_left += 'px';

				$attached_dl_set.children('dl').eq(n_idx).css({
					'position': 'absolute',
					'top'     : t_top,
					'left'    : t_left,
					'display' : 'block'
				});
			}
		}
	};

	//================================================================================
	//10.処理の始まり
	//--------------------------------------------------------------------------------
	$.fn.ajaxComboBox = function(source, options) {
		if (!source) return;

		//************************************************************
		//オプション
		//************************************************************
		//----------------------------------------
		//初回
		//----------------------------------------
		options = $.extend({
			//基本設定
			source         : source,
			db_table       : 'tbl',                    //接続するDBのテーブル名
			img_dir        : 'acbox/img/',             //ボタン画像へのパス
			field          : 'name',                   //候補として表示するカラム名
			and_or         : 'AND',                    //複数の検索後に対応するため
			minchars       : 1,                        //候補予測処理を始めるのに必要な最低の文字数
			per_page       : 10,                       //候補一覧1ページに表示する件数
			navi_num       : 5,                        //ページナビで表示するページ番号の数
			navi_simple    : false,                    //先頭、末尾のページへのリンクを表示するか？
			init_val       : false,                    //ComboBoxの初期値(配列形式で渡す)
			init_src       : 'acbox/php/initval.php',  //初期値設定で、セレクト専用の場合に必要
			lang           : 'ja',                     //言語を選択(デフォルトは日本語)
			bind_to        : false,                    //候補選択後に実行されるイベントの名前
			
			//サブ情報
			sub_info       : false, //サブ情報を表示するかどうか？
			sub_as         : {},    //サブ情報での、カラム名の別名
			show_field     : '',    //サブ情報で表示するカラム(複数指定はカンマ区切り)
			hide_field     : '',    //サブ情報で非表示にするカラム(複数指定はカンマ区切り)

			//セレクト専用
			select_only    : false, //セレクト専用にするかどうか？
			primary_key    : 'id',  //セレクト専用時、hiddenの値となるカラム
			
			//ComboBox関連
			container_class: 'ac_container', //ComboBoxの<table>
			input_class    : 'ac_input', //テキストボックス
			button_class   : 'ac_button', //ボタンのCSSクラス
			btn_on_class   : 'ac_btn_on', //ボタン(mover時)
			btn_out_class  : 'ac_btn_out', //ボタン(mout時)
			re_area_class  : 'ac_result_area', //結果リストの<div>
			navi_class     : 'ac_navi', //ページナビを囲む<div>
			results_class  : 'ac_results', //候補一覧を囲む<ul>
			select_class   : 'ac_over', //選択中の<li>
			match_class    : 'ac_match', //ヒット文字の<span>
			sub_info_class : 'ac_attached', //サブ情報
			select_ok_class: 'ac_select_ok',
			select_ng_class: 'ac_select_ng',
			container_open_class:'ac_container_open'
		}, options);
		
		//----------------------------------------
		//2回目の設定(他のオプションの値を引用するため)
		//----------------------------------------
		options = $.extend({
			search_field : options.field, //検索するフィールド(カンマ区切りで複数指定可能)
			order_field  : options.field, //ORDER BY(SQL) の基準となるカラム名(カンマ区切りで複数指定可能)
			order_by     : 'ASC',         //ORDER BY(SQL) で、並ベ替えるのは昇順か降順か

			//画像
			button_img   : options.img_dir + 'combobox_button' + '.png',
			load_img     : options.img_dir + 'ajax-loader'     + '.gif'
		}, options);

		//************************************************************
		//メッセージを言語別に用意
		//************************************************************
		switch (options.lang){
		
			//日本語
			case 'ja':
				var msg = {
					'add_btn'     : '追加ボタン',
					'add_title'   : '入力ボックスを追加します',
					'del_btn'     : '削除ボタン',
					'del_title'   : '入力ボックスを削除します',
					'next'        : '次へ',
					'next_title'  : '次の'+options.per_page+'件 (右キー)',
					'prev'        : '前へ',
					'prev_title'  : '前の'+options.per_page+'件 (左キー)',
					'first_title' : '最初のページへ (Shift + 左キー)',
					'last_title'  : '最後のページへ (Shift + 右キー)',
					'get_all_btn' : '全件取得 (下キー)',
					'get_all_alt' : '画像:ボタン',
					'close_btn'   : '閉じる (Tabキー)',
					'close_alt'   : '画像:ボタン',
					'loading'     : 'ロード中...',
					'loading_alt' : '画像:ロード中...',
					'page_info'   : 'num_page_top - num_page_end 件 (全 cnt 件)',
					'select_ng'   : '注意 : リストの中から選択してください',
					'select_ok'   : 'OK : 正しく選択されました。'
				};
				break;

			//英語
			case 'en':
				var msg = {
					'add_btn'     : 'Add button',
					'add_title'   : 'add a box',
					'del_btn'     : 'Del button',
					'del_title'   : 'delete a box',
					'next'        : 'Next',
					'next_title'  : 'Next'+options.per_page+' (Right key)',
					'prev'        : 'Prev',
					'prev_title'  : 'Prev'+options.per_page+' (Left key)',
					'first_title' : 'First (Shift + Left key)',
					'last_title'  : 'Last (Shift + Right key)',
					'get_all_btn' : 'Get All (Down key)',
					'get_all_alt' : '(button)',
					'close_btn'   : 'Close (Tab key)',
					'close_alt'   : '(button)',
					'loading'     : 'loading...',
					'loading_alt' : '(loading)',
					'page_info'   : 'num_page_top - num_page_end of cnt',
					'select_ng'   : 'Attention : Please choose from among the list.',
					'select_ok'   : 'OK : Correctly selected.'
				};
				break;

			//スペイン語 (Joaquin G. de la Zerda氏からの提供)
			case 'es':
				var msg = {
					'add_btn'     : 'Agregar boton',
					'add_title'   : 'Agregar una opcion',
					'del_btn'     : 'Borrar boton',
					'del_title'   : 'Borrar una opcion',
					'next'        : 'Siguiente',
					'next_title'  : 'Proximas '+options.per_page+' (tecla derecha)',
					'prev'        : 'Anterior',
					'prev_title'  : 'Anteriores '+options.per_page+' (tecla izquierda)',
					'first_title' : 'Primera (Shift + Left)',
					'last_title'  : 'Ultima (Shift + Right)',
					'get_all_btn' : 'Ver todos (tecla abajo)',
					'get_all_alt' : '(boton)',
					'close_btn'   : 'Cerrar (tecla TAB)',
					'close_alt'   : '(boton)',
					'loading'     : 'Cargando...',
					'loading_alt' : '(Cargando)',
					'page_info'   : 'num_page_top - num_page_end de cnt',
					'select_ng'   : 'Atencion: Elija una opcion de la lista.',
					'select_ok'   : 'OK: Correctamente seleccionado.'
				};
				break;

			default:
		}
		this.each(function() {
			new $.ajaxComboBox(this, source, options, msg);
		});
		return this;
	};
})(jQuery);
